﻿// Copyright (c) 2009 Sun Microsystems, Inc.
//
// MySQL Connector/NET is licensed under the terms of the GPLv2
// <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most 
// MySQL Connectors. There are special exceptions to the terms and 
// conditions of the GPLv2 as it is applied to this software, see the 
// FLOSS License Exception
// <http://www.mysql.com/about/legal/licensing/foss-exception.html>.
//
// This program is free software; you can redistribute it and/or modify 
// it under the terms of the GNU General Public License as published 
// by the Free Software Foundation; version 2 of the License.
//
// This program is distributed in the hope that it will be useful, but 
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
// for more details.
//
// You should have received a copy of the GNU General Public License along 
// with this program; if not, write to the Free Software Foundation, Inc., 
// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA

using System;
using System.Collections;
using System.Data;
using MySql.Data.MySqlClient.Properties;
using MySql.Data.Types;
using System.Diagnostics;
using System.Collections.Generic;

namespace MySql.Data.MySqlClient
{
    internal class ResultSet
    {
        private Driver driver;
        private bool hasRows;
        private bool[] uaFieldsUsed;
        private MySqlField[] fields;
        private IMySqlValue[] values;
        private Hashtable fieldHashCS;
        private Hashtable fieldHashCI;
        private int rowIndex;
        private bool readDone;
        private bool isSequential;
        private int seqIndex;
        private bool isOutputParameters;
        private int affectedRows;
        private int insertedId;
        private int statementId;
        private int totalRows;
        private int skippedRows;
        private bool cached;
        private List<IMySqlValue[]> cachedValues;

        public ResultSet(int affectedRows, int insertedId)
        {
            this.affectedRows = affectedRows;
            this.insertedId = insertedId;
            readDone = true;
        }

        public ResultSet(Driver d, int statementId, int numCols)
        {
            affectedRows = -1;
            insertedId = -1;
            driver = d;
            this.statementId = statementId;
            rowIndex = -1;
            LoadColumns(numCols);
            isOutputParameters = IsOutputParameterResultSet();
            hasRows = GetNextRow();
            readDone = !hasRows;
        }

        #region Properties

        public bool HasRows
        {
            get { return hasRows; }
        }

        public int Size
        {
            get { return fields == null ? 0 : fields.Length; }
        }

        public MySqlField[] Fields
        {
            get { return fields; }
        }

        public IMySqlValue[] Values
        {
            get { return values; }
        }

        public bool IsOutputParameters
        {
            get { return isOutputParameters; }
            set { isOutputParameters = value; }
        }

        public int AffectedRows
        {
            get { return affectedRows; }
        }

        public int InsertedId
        {
            get { return insertedId; }
        }

        public int TotalRows
        {
            get { return totalRows; }
        }

        public int SkippedRows
        {
            get { return skippedRows; }
        }

        public bool Cached
        {
            get { return cached; }
            set 
            { 
                cached = value;
                if (cached && cachedValues == null)
                    cachedValues = new List<IMySqlValue[]>();
            }
        }

        #endregion

        /// <summary>
        /// return the ordinal for the given column name
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public int GetOrdinal(string name)
        {
            // first we try a quick hash lookup
            object ordinal = fieldHashCS[name];
            if (ordinal != null)
                return (int)ordinal;

            // ok that failed so we use our CI hash
            ordinal = fieldHashCI[name];
            if (ordinal != null)
                return (int)ordinal;

            // Throw an exception if the ordinal cannot be found.
            throw new IndexOutOfRangeException(
                String.Format(Resources.CouldNotFindColumnName, name));
        }

        /// <summary>
        /// Retrieve the value as the given column index
        /// </summary>
        /// <param name="index">The column value to retrieve</param>
        /// <returns>The value as the given column</returns>
        public IMySqlValue this[int index]
        {
            get
            {
                if (rowIndex < 0)
                    throw new MySqlException(Resources.AttemptToAccessBeforeRead);

                // keep count of how many columns we have left to access
                uaFieldsUsed[index] = true;

                if (isSequential && index != seqIndex)
                {
                    if (index < seqIndex)
                        throw new MySqlException(Resources.ReadingPriorColumnUsingSeqAccess);
                    while (seqIndex < (index - 1))
                        driver.SkipColumnValue(values[++seqIndex]);
                    values[index] = driver.ReadColumnValue(index, fields[index], values[index]);
                    seqIndex = index;
                }

                return values[index];
            }
        }

        private bool GetNextRow()
        {
            bool fetched = driver.FetchDataRow(statementId, Size);
            if (fetched)
                totalRows++;
            return fetched;
        }


        public bool NextRow(CommandBehavior behavior)
        {
            if (readDone)
            {
                if (Cached) return CachedNextRow(behavior);
                return false;
            }

            if ((behavior & CommandBehavior.SingleRow) != 0 && rowIndex == 0)
                return false;

            isSequential = (behavior & CommandBehavior.SequentialAccess) != 0;
            seqIndex = -1;

            // if we are at row index >= 0 then we need to fetch the data row and load it
            if (rowIndex >= 0)
            {
                bool fetched = false;
                try
                {
                    fetched = GetNextRow();
                }
                catch (MySqlException ex)
                {
                    if (ex.IsQueryAborted)
                    {
                        // avoid hanging on Close()
                        readDone = true;
                    }
                    throw;
                }

                if (!fetched)
                {
                    readDone = true;
                    return false;
                }
            }

            if (!isSequential) ReadColumnData(false);
            rowIndex++;
            return true;
        }

        private bool CachedNextRow(CommandBehavior behavior)
        {
            if ((behavior & CommandBehavior.SingleRow) != 0 && rowIndex == 0)
                return false;
            if (rowIndex == (totalRows - 1)) return false;
            rowIndex++;
            values = cachedValues[rowIndex];
            return true;
        }

        /// <summary>
        /// Closes the current resultset, dumping any data still on the wire
        /// </summary>
        public void Close()
        {
            if (!readDone)
            {

                // if we have rows but the user didn't read the first one then mark it as skipped
                if (HasRows && rowIndex == -1)
                    skippedRows++;
                try
                {
                    while (driver.IsOpen && driver.SkipDataRow())
                    {
                        totalRows++;
                        skippedRows++;
                    }
                }
                catch (System.IO.IOException)
                {
                    // it is ok to eat IO exceptions here, we just want to 
                    // close the result set
                }
                readDone = true;
            }
            else if (driver == null)
                CacheClose();

            driver = null;
            if (Cached) CacheReset();
        }

        private void CacheClose()
        {
            skippedRows = totalRows - rowIndex - 1;
        }

        private void CacheReset()
        {
            if (!Cached) return;
            rowIndex = -1;
            affectedRows = -1;
            insertedId = -1;
            skippedRows = 0;
        }

        public bool FieldRead(int index)
        {
            Debug.Assert(Size > index);
            return uaFieldsUsed[index];
        }

        public void SetValueObject(int i, IMySqlValue valueObject)
        {
            Debug.Assert(values != null);
            Debug.Assert(i < values.Length);
            values[i] = valueObject;
        }

        private bool IsOutputParameterResultSet()
        {
            if (driver.HasStatus(ServerStatusFlags.OutputParameters)) return true;

            if (fields.Length == 0) return false;

            for (int x = 0; x < fields.Length; x++)
                if (!fields[x].ColumnName.StartsWith("@" + StoredProcedure.ParameterPrefix)) return false;
            return true;
        }

        /// <summary>
        /// Loads the column metadata for the current resultset
        /// </summary>
        private void LoadColumns(int numCols)
        {
            fields = driver.GetColumns(numCols);

            values = new IMySqlValue[numCols];
            uaFieldsUsed = new bool[numCols];
            fieldHashCS = new Hashtable();
            fieldHashCI = new Hashtable(StringComparer.InvariantCultureIgnoreCase);

            for (int i = 0; i < fields.Length; i++)
            {
                string columnName = fields[i].ColumnName;
                if (!fieldHashCS.ContainsKey(columnName))
                    fieldHashCS.Add(columnName, i);
                if (!fieldHashCI.ContainsKey(columnName))
                    fieldHashCI.Add(columnName, i);
                values[i] = fields[i].GetValueObject();
            }
        }

        private void ReadColumnData(bool outputParms)
        {
            for (int i = 0; i < Size; i++)
                values[i] = driver.ReadColumnValue(i, fields[i], values[i]);

            // if we are caching then we need to save a copy of this row of data values
            if (Cached)
                cachedValues.Add((IMySqlValue[])values.Clone());

            // we don't need to worry about caching the following since you won't have output
            // params with TableDirect commands
            if (outputParms)
            {
                bool rowExists = driver.FetchDataRow(statementId, fields.Length);
                rowIndex = 0;
                if (rowExists)
                    throw new MySqlException(Resources.MoreThanOneOPRow);
            }
        }
    }
}
